﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NHibernate.Criterion;
using NHibernate;
using System.Data;
using System.Reflection;
using NHibernate.Mapping;
using System.Collections;
using NHibernate.Dialect.Function;
using NHibernate.Dialect;

namespace Apollo.Core
{
    public class ProjectionManager
    {
        protected static string PATH_SEPARATOR = "____";

        /// <summary>
        /// All valid projections: column projections, filter projections, order projections
        /// </summary>
        public IList<ProjectionManagerItem> Items { get; private set; }

        protected Type baseType;

        protected FilterGroup filter;
        protected ProjectionManagerItem orderProjection;
        protected OrderType orderType = OrderType.Asc;
        protected Projection identifierProjection;

        protected ICriteria criteria;

        // this will hold all the aliasses
        protected Dictionary<string, string> aliases = new Dictionary<string, string>();

        public ProjectionManager(string projectionList, Type baseType) {
            this.baseType = baseType;

            this.Items = new List<ProjectionManagerItem>();

            string[] projections = projectionList.Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);

            foreach (string proj in projections)
            {
                ProjectionManagerItem item = new ProjectionManagerItem(proj, baseType);

                if (item.Projection.IsValid) // TODO add also Logical checks
                    this.Items.Add(item);
            }
        }

        public void AddOrder(string projection, bool isAsc) {
            orderType = isAsc ? OrderType.Asc : OrderType.Desc;

            orderProjection = new ProjectionManagerItem(projection, baseType);

            if (orderProjection.Aggregation <= 0 && this.HasAggregatedColumns) {
                // check wheather this projection is a group by clause
                bool foundAsGroupBy = false;
                foreach (ProjectionManagerItem item in this.Items)
                {
                    if (item.Aggregation == orderProjection.Aggregation 
                        && item.Projection.ProjectionPath == orderProjection.Projection.ProjectionPath)
                    {
                        foundAsGroupBy = true;
                        return;
                    }
                }

                if (!foundAsGroupBy)
                {
                    orderProjection = null;
                    return;
                }
            }

            if (!orderProjection.Projection.IsValid)
                orderProjection = null;

        }

        public void AddFilter(string filterJSON) {
            if (this.filter == null)
                this.filter = WebHelper.DeserializeGroupFromJSON(filterJSON, this.baseType);
            else {
                FilterGroup newFilter = WebHelper.DeserializeGroupFromJSON(filterJSON, this.baseType);

                if(this.filter.Operator == GroupOperator.And){
                    this.filter.Groups.Add(newFilter);
                }
                else {
                    FilterGroup oldFilter = this.filter;

                    this.filter = new FilterGroup() { Operator = GroupOperator.And };
                    if (!oldFilter.IsEmpty)
                        this.filter.Groups.Add(oldFilter);
                    this.filter.Groups.Add(newFilter);
                }
            }
        }

        public int RowCount()
        {
            if (!this.HasAggregatedColumns)
            {
                ICriteria rowCountCriteria = (ICriteria)this.CreateCriteriaWithoutOrderBy().Clone();

                return (int)rowCountCriteria
                    .SetProjection(NHibernate.Criterion.Projections.RowCount())
                    .UniqueResult();
            }
            else {
                
                /*try
                {
                    ICriteria rowCountCriteria = (ICriteria)this.CreateCriteriaWithoutOrderBy().Clone();

                    if (this.HasCalculationDateColumns)
                        this.LoadCustomSQLTemplatesIntoDialect();

                    // create the projection list
                    ProjectionList nhibernateProjectionList = Projections.ProjectionList();

                    foreach (ProjectionManagerItem item in this.Items)
                    {
                        if (item.Aggregation > 0)
                            continue;

                        switch (item.Aggregation) { 
                            case ProjectionAggregate.Date:
                            case ProjectionAggregate.Month:
                            case ProjectionAggregate.Quarter:
                            case ProjectionAggregate.Year:
                                nhibernateProjectionList.Add(
                                    Projections.SqlFunction("apolloCountDistinct",
                                        NHibernateUtil.DateTime,
                                        new IProjection[] {
                                            Projections.SqlFunction(
                                                "apollo" + item.Aggregation.ToString(),
                                                NHibernateUtil.DateTime,
                                                Projections.Property(item.DisplayProjection.Alias + "." + item.DisplayProjection.PropertyName)
                                            )}
                                    )
                                );
                                break;
                            default:
                                nhibernateProjectionList.Add(Projections.CountDistinct(
                                    item.DisplayProjection.Alias + "." + item.DisplayProjection.PropertyName));
                                break;
                        }
                    }

                    object obj = rowCountCriteria.SetProjection(nhibernateProjectionList)
                        .SetResultTransformer(NHibernate.Transform.Transformers.ToList)
                        .UniqueResult();

                    int maxRows = 0;
                    foreach (int numRows in (IEnumerable)obj)
                    {
                        if (numRows > maxRows)
                            maxRows = numRows;
                    }

                    return maxRows;
                }
                catch
                {*/
                    return -1;
                //}
            }
        }

        protected void LoadCustomSQLTemplatesIntoDialect() {
            Dialect currentDialect = Apollo.Application.CurrentSession.GetSessionImplementation().Factory.Dialect;

            foreach(Apollo.Configuration.SqlTemplate template in Apollo.Application.ConfigurationSection.Apollo.SqlTemplates) {
                if (!currentDialect.Functions.ContainsKey(template.Name))
                    currentDialect.Functions.Add(
                        template.Name,
                        new SQLFunctionTemplate(NHibernateUtil.Int32
                            , template.Value)
                    );
            }
        }

        public ICriteria CreateCriteria()
        {
            ICriteria orderedCriteria = (ICriteria)this.CreateCriteriaWithoutOrderBy().Clone();

            if (orderProjection != null)
            {
                string propertyName = orderProjection.DisplayProjection.Alias + "." + orderProjection.DisplayProjection.PropertyName;

                if (orderProjection.Aggregation == ProjectionAggregate.None)
                {
                    if (orderType == OrderType.Asc)
                        orderedCriteria.AddOrder(Order.Asc(Projections.Property(propertyName)));
                    else
                        orderedCriteria.AddOrder(Order.Desc(Projections.Property(propertyName)));
                }
                else {
                    switch (orderProjection.Aggregation) { 
                        case ProjectionAggregate.Sum:
                            if (orderType == OrderType.Asc)
                                orderedCriteria.AddOrder(Order.Asc(Projections.Sum(propertyName)));
                            else
                                orderedCriteria.AddOrder(Order.Desc(Projections.Sum(propertyName)));
                            break;
                        case ProjectionAggregate.Count:
                            if (orderType == OrderType.Asc)
                                orderedCriteria.AddOrder(Order.Asc(Projections.Count(propertyName)));
                            else
                                orderedCriteria.AddOrder(Order.Desc(Projections.Sum(propertyName)));
                            break;
                        case ProjectionAggregate.CountDistinct:
                            if (orderType == OrderType.Asc)
                                orderedCriteria.AddOrder(Order.Asc(Projections.CountDistinct(propertyName)));
                            else
                                orderedCriteria.AddOrder(Order.Desc(Projections.CountDistinct(propertyName)));
                            break;
                        case ProjectionAggregate.Min:
                            if (orderType == OrderType.Asc)
                                orderedCriteria.AddOrder(Order.Asc(Projections.Min(propertyName)));
                            else
                                orderedCriteria.AddOrder(Order.Desc(Projections.Min(propertyName)));
                            break;
                        case ProjectionAggregate.Max:
                            if (orderType == OrderType.Asc)
                                orderedCriteria.AddOrder(Order.Asc(Projections.Max(propertyName)));
                            else
                                orderedCriteria.AddOrder(Order.Desc(Projections.Max(propertyName)));
                            break;
                        case ProjectionAggregate.Avg:
                            if (orderType == OrderType.Asc)
                                orderedCriteria.AddOrder(Order.Asc(Projections.Avg(propertyName)));
                            else
                                orderedCriteria.AddOrder(Order.Desc(Projections.Avg(propertyName)));
                            break;
                        case ProjectionAggregate.Year:
                        case ProjectionAggregate.Quarter:
                        case ProjectionAggregate.Month:
                        case ProjectionAggregate.Date:
                            if (orderType == OrderType.Asc)
                                orderedCriteria.AddOrder(Order.Asc(Projections.SqlFunction(
                                    "Apollo" + orderProjection.Aggregation.ToString() + "SQLTemplate", NHibernateUtil.DateTime, Projections.Property(propertyName))));
                            else
                                orderedCriteria.AddOrder(Order.Desc(Projections.SqlFunction(
                                    "Apollo" + orderProjection.Aggregation.ToString() + "SQLTemplate", NHibernateUtil.DateTime, Projections.Property(propertyName))));
                            break;
                    }
                }
            }
            
            return orderedCriteria;
        }

        protected ICriteria CreateCriteriaWithoutOrderBy()
        {
            if (criteria != null)
                return criteria;

            criteria = Application.CurrentSession
                .CreateCriteria(baseType, "this")
                .SetFetchMode("this", FetchMode.Lazy);


            foreach (ProjectionManagerItem item in this.Items)
            {
                this.MapProjectionToAlias(item.DisplayProjection);

                if(item.IdentifierProjection != null)
                    this.MapProjectionToAlias(item.IdentifierProjection);
            }
            if (orderProjection != null)
                this.MapProjectionToAlias(orderProjection.DisplayProjection);
            if (filter != null)
                foreach (Projection projection in filter.Projections)
                    this.MapProjectionToAlias(projection);


            foreach (KeyValuePair<string, string> alias in aliases)
            {
                criteria = criteria
                    .CreateAlias(alias.Value, alias.Key, NHibernate.SqlCommand.JoinType.LeftOuterJoin)
                    .SetFetchMode(alias.Key, FetchMode.Eager);
            }
            

            if (filter != null && !filter.IsEmpty)
            {
                ICriterion filterCriterion = filter.ToCriterion();
                if(filterCriterion != null)
                    criteria.Add(filterCriterion);
            }

            return criteria;
        }

        protected ICriteria CreateLightCriteria() {
            // get the default criteria
            ICriteria criteria = this.CreateCriteria();

            // without aggregated columns, the projection list 
            // will contain also the identifier column
            int columnIndex = this.HasAggregatedColumns ? 0 : 1;

            // set a unique alias to each of the current items
            foreach (ProjectionManagerItem item in this.Items)
            {
                item.DisplayProjectionAlias = item.DisplayProjection.PropertyName + "_" + columnIndex.ToString();
                item.DisplayProjectionIndex = columnIndex;
                columnIndex++;

                if (item.IdentifierProjection != null)
                {
                    item.IdentifierProjectionAlias = item.IdentifierProjection.PropertyName + "_" + columnIndex.ToString();
                    item.IdentifierProjectionIndex = columnIndex;
                    columnIndex++;
                }
            }

            // create the projection list
            ProjectionList nhibernateProjectionList = Projections.ProjectionList();

            if (!this.HasAggregatedColumns)
            {
                // adding the default identifier
                nhibernateProjectionList.Add(
                          Projections.Property("this." + this.IdentifierProjection.PropertyName)
                        , IdentifierProjection.PropertyName + "_0");
            }
            
            if (this.HasCalculationDateColumns) 
                this.LoadCustomSQLTemplatesIntoDialect();            

            foreach (ProjectionManagerItem item in this.Items)
            {
                switch (item.Aggregation)
                {
                    #region Group aggregates
                    case ProjectionAggregate.Sum:
                        nhibernateProjectionList.Add(
                                Projections.Sum(item.DisplayProjection.Alias + "." + item.DisplayProjection.PropertyName)
                                , item.DisplayProjectionAlias);
                        break;
                    case ProjectionAggregate.Count:
                        nhibernateProjectionList.Add(
                                Projections.Count(item.DisplayProjection.Alias + "." + item.DisplayProjection.PropertyName)
                                , item.DisplayProjectionAlias);
                        break;
                    case ProjectionAggregate.CountDistinct:
                        nhibernateProjectionList.Add(
                                Projections.CountDistinct(item.DisplayProjection.Alias + "." + item.DisplayProjection.PropertyName)
                                , item.DisplayProjectionAlias);
                        break;
                    case ProjectionAggregate.Min:
                        nhibernateProjectionList.Add(
                                Projections.Min(item.DisplayProjection.Alias + "." + item.DisplayProjection.PropertyName)
                                , item.DisplayProjectionAlias);
                        break;
                    case ProjectionAggregate.Max:
                        nhibernateProjectionList.Add(
                                Projections.Max(item.DisplayProjection.Alias + "." + item.DisplayProjection.PropertyName)
                                , item.DisplayProjectionAlias);
                        break;
                    case ProjectionAggregate.Avg:
                        nhibernateProjectionList.Add(
                                Projections.Avg(item.DisplayProjection.Alias + "." + item.DisplayProjection.PropertyName)
                                , item.DisplayProjectionAlias);
                        break;
                    #endregion
                    #region Value aggregates
                    case ProjectionAggregate.Year:
                    case ProjectionAggregate.Quarter:
                    case ProjectionAggregate.Month:
                    case ProjectionAggregate.Date:
                        if (this.HasAggregatedColumns)
                        {
                            nhibernateProjectionList.Add(
                                Projections.GroupProperty(
                                    Projections.SqlFunction(
                                        "Apollo" + item.Aggregation.ToString() + "SQLTemplate",
                                        NHibernateUtil.DateTime,
                                        Projections.Property(item.DisplayProjection.Alias + "." + item.DisplayProjection.PropertyName)
                                    )
                                )
                                , item.DisplayProjectionAlias);
                        }
                        else {
                            nhibernateProjectionList.Add(
                                    Projections.SqlFunction(
                                        "Apollo" + item.Aggregation.ToString() + "SQLTemplate",
                                        NHibernateUtil.DateTime,
                                        Projections.Property(item.DisplayProjection.Alias + "." + item.DisplayProjection.PropertyName)
                                    )
                                    , item.DisplayProjectionAlias);                        
                        }
                        break;
                    #endregion
                    default: // None
                        if (this.HasAggregatedColumns)
                        {
                            nhibernateProjectionList.Add(
                                Projections.GroupProperty(item.DisplayProjection.Alias + "." + item.DisplayProjection.PropertyName)
                                , item.DisplayProjectionAlias);

                            if (item.IdentifierProjection != null)
                            {
                                nhibernateProjectionList.Add(
                                    Projections.GroupProperty(item.IdentifierProjection.Alias + "." 
                                            + item.IdentifierProjection.PropertyName)
                                    , item.IdentifierProjectionAlias);
                            }
                        }
                        else 
                        {
                            nhibernateProjectionList.Add(
                                Projections.Property(item.DisplayProjection.Alias + "." + item.DisplayProjection.PropertyName)
                                , item.DisplayProjectionAlias);

                            if (item.IdentifierProjection != null)
                            {
                                nhibernateProjectionList.Add(
                                    Projections.Property(item.IdentifierProjection.Alias + "."
                                            + item.IdentifierProjection.PropertyName)
                                    , item.IdentifierProjectionAlias);
                            }
                        }
                        break;

                }
            }
            

            criteria = criteria.SetProjection(nhibernateProjectionList)
                .SetResultTransformer(NHibernate.Transform.Transformers.ToList);

            return criteria;
        }

        protected void MapProjectionToAlias(Projection from) {
            // get actual path
            string path = from.ProjectionPathWithoutProperty;

            // if it is a base path
            if(string.IsNullOrEmpty(path)){
                from.Alias = "this";
                return;
            }

            from.Alias = path.Replace(".", PATH_SEPARATOR);

            if (!aliases.ContainsKey(from.Alias))
            {
                // add it to 
                string[] parts = path.Split(".".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);

                if (parts.Length == 0)
                {
                    from.Alias = "this";
                    return;
                }

                string alias = parts[0];
                if (!aliases.ContainsKey(alias))
                    aliases.Add(alias, alias);

                for (int index = 1; index < parts.Length; index++) {
                    string alias_property = alias + "." + parts[index];
                    alias += PATH_SEPARATOR + parts[index];

                    if (!aliases.ContainsKey(alias))
                        aliases.Add(alias, alias_property);
                }
            }
        }

        protected Projection IdentifierProjection {
            get {
                if (identifierProjection == null) {
                    Entity baseEntity = Apollo.Application.Meta<Entity>()
                        .SingleOrDefault<Entity>(e => e.Name == baseType.Name);

                    identifierProjection = new Projection(baseEntity.IdentifierProperty, baseType);
                }

                return identifierProjection;
            }
        }

        public string ToJSON(int skip, int take)
        {
            IList result = this.CreateLightCriteria()
                .SetFirstResult(skip)
                .SetMaxResults(take)
                .List();

            IList<ProjectionManagerItem> nonAggregatedProjections = new List<ProjectionManagerItem>();

            if (this.HasAggregatedColumns) {
                foreach (ProjectionManagerItem item in this.Items) {
                    if (item.Aggregation <= 0) {
                        nonAggregatedProjections.Add(item);
                    }
                }
            }

            string json = "";
            foreach (IList row in result) {
                if (json != "")
                    json += ",";

                if (this.HasAggregatedColumns) {

                    string rowId = "";
                    foreach (ProjectionManagerItem item in nonAggregatedProjections) {
                        if (rowId != "")
                            rowId += ",";

                        if (item.IdentifierProjection != null)
                            rowId += this.Serialize(row[item.IdentifierProjectionIndex]
                                        , item.IdentifierProjection.EntityProperty.FormatString);
                        else
                            rowId += this.Serialize(row[item.DisplayProjectionIndex]
                                        , item.DisplayProjection.EntityProperty.FormatString);
                    }

                    json += @"{""id"":""" + rowId + @""",""cell"":[";

                }
                else
                    json += @"{""id"":""" + this.Serialize(row[0], this.IdentifierProjection.EntityProperty.FormatString) + @""",""cell"":[";

                string json_row = "";
                foreach (ProjectionManagerItem item in this.Items) {
                    if (json_row != "")
                        json_row += ",";

                    /*if (item.Aggregation < 0) { 
                        // treat here custom datetime
                        switch (item.Aggregation) { 
                            case ProjectionAggregate.Date:
                                int date = (int?)row[item.DisplayProjectionIndex] ?? 0;
                                int date_year = date / 10000;
                                int date_month = (date / 100) % 100;
                                int date_day = date % 100;

                                json_row += "\"" + string.Format("{0}/{1}/{2}", date_day, date_month, date_year) + "\"";
                                break;
                            case ProjectionAggregate.Month:
                                int month = (int?)row[item.DisplayProjectionIndex] ?? 0;
                                int month_year = month / 100;
                                int month_month = month % 100;

                                json_row += "\"" + string.Format("{0}/{1}", month_month, month_year) + "\"";
                                break;
                            case ProjectionAggregate.Quarter:
                                int quarter = (int?)row[item.DisplayProjectionIndex] ?? 0;
                                int quarter_year = quarter / 10;
                                int quarter_month = quarter % 10;

                                json_row += "\"" + string.Format("Q{0}/{1}", quarter_month, quarter_year) + "\"";
                                break;
                            default:
                                json_row += "\"" + string.Format("{0}",row[item.DisplayProjectionIndex])  + "\"";
                                break;
                        }
                    }
                    else*/ if (item.IdentifierProjection != null) {
                        json_row += "\"" + this.Serialize(
                            row[item.DisplayProjectionIndex]
                            , row[item.IdentifierProjectionIndex]
                            , item.DisplayProjection.EntityProperty.FormatString
                            , item.IdentifierProjection.EntityProperty.FormatString
                            , item.Projection.RelationEntity) + "\"";
                    }
                    else {
                        json_row += "\"" + this.Serialize(row[item.DisplayProjectionIndex], item.DisplayProjection.EntityProperty.FormatString) + "\"";
                    }
                }

                json += json_row + "]}";
            }

            return json;
        }

        public string ToCSV()
        {
            string csv = "";

            IList results = this.CreateLightCriteria()
                .List();

            foreach (IList row in results)
            {
                if (csv != "")
                    csv += "\n";

                string csv_row = "";
                foreach (ProjectionManagerItem item in this.Items)
                {
                    if (csv_row != "")
                        csv_row += ",";

                    csv_row += "\"" + this.Serialize(row[item.DisplayProjectionIndex], item.DisplayProjection.EntityProperty.FormatString) + "\"";
                }

                csv += csv_row;
            }

            
            return csv;
        }

        private string Serialize(object displayValue, string format) {
            string value = "";

            if (string.IsNullOrEmpty(format))
                value = Apollo.Application.Serialize(displayValue, true);
            else
                value = Apollo.Application.Serialize(displayValue, format);

            if (!string.IsNullOrEmpty(value)) {
                if (value.Length > 30)
                {
                    value = value.Substring(0, 30);
                    value += "...";
                }
                value = value.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("<", "&lt;");
            }

            return value;
        }

        private string Serialize(object displayValue, object idValue, string formatDisplayValue, string formatIdValue, Entity entity)
        {
            string serializedDisplayValue = this.Serialize(displayValue, formatDisplayValue);
            string serializedIdValue = this.Serialize(idValue, formatIdValue);

            if (string.IsNullOrEmpty(serializedDisplayValue) && !string.IsNullOrEmpty(serializedIdValue))
                serializedDisplayValue = "{" + serializedIdValue + "}";

            string editUrl = string.IsNullOrEmpty(entity.CustomEditUrl) ?
                                    "Edit.aspx?EntityId=" + entity.Id.ToString() + "&ObjectId={ObjectId}" :
                                    entity.CustomEditUrl;
            editUrl = editUrl.Replace("{ObjectId}", serializedIdValue);

            return string.Format("<a href='javascript:ModalOpen(\\\"{0}\\\", false)'>{1}</a>", editUrl, serializedDisplayValue);
        }

        protected bool? hasAggregatedColumns;
        /// <summary>
        /// True if the manager contains at least one group aggregate
        /// </summary>
        public bool HasAggregatedColumns {
            get {
                if (hasAggregatedColumns == null)
                {
                    hasAggregatedColumns = false;
                    foreach (ProjectionManagerItem item in this.Items)
                        if (item.Aggregation > 0)
                            hasAggregatedColumns = true;
                }

                return hasAggregatedColumns.Value;
            }
        }

        protected bool? hasCalculationDateColumns;
        /// <summary>
        /// True if the manager contains at least one group aggregate
        /// </summary>
        public bool HasCalculationDateColumns
        {
            get
            {
                if (hasCalculationDateColumns == null)
                {
                    hasCalculationDateColumns = false;
                    foreach (ProjectionManagerItem item in this.Items)
                        if (item.Aggregation < 0)
                            hasCalculationDateColumns = true;
                }

                return hasCalculationDateColumns.Value;
            }
        } 

        protected enum OrderType { Asc, Desc }
    }
}
